package com.sa35.fbs.model;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.type.NullableType;
import org.hibernate.type.TypeFactory;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;

/**
 * A class that allows Hibernate to handle mapping between primitives and Java
 * Enum objects.
 *
 * @author Johan
 *
 */
public class GenericEnumType implements UserType, ParameterizedType {
	private static final String ID_METHOD = "identifier";
	private static final String VALUE_OF_METHOD = "valueOf";
	private static final String DEFAULT_SETTER_METHOD = "name";
	private static final String DEFAULT_GETTER_METHOD = "valueOf";

	@SuppressWarnings("rawtypes")
	private Class<? extends Enum> enumClass;
	private Class<?> identifierType;
	private int[] sqlTypes;
	private Method identifierMethod;
	private Method valueOfMethod;
	private NullableType type;

	/**
	 * @param owner unused
	 */
	@Override
	public Object assemble(Serializable cached, Object owner) throws HibernateException {
		return cached;
	}

	@Override
	public Object deepCopy(Object value) throws HibernateException {
		return value;
	}

	@Override
	public Serializable disassemble(Object value) throws HibernateException {
		return (Enum<?>) value;
	}

	@Override
	public boolean equals(Object x, Object y) throws HibernateException { // NOSONAR
		return x == y;
	}

	@Override
	public int hashCode(Object x) throws HibernateException {
		return x.hashCode();
	}

	@Override
	public boolean isMutable() {
		return false;
	}

	/**
	 * @param owner unused
	 */
	@Override
	public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
		Object identifier = type.get(rs, names[0]);
		if(rs.wasNull())
			return null;
		try {
			return valueOfMethod.invoke(enumClass, new Object[] { identifier });
		} catch(Exception e) {
			throw new HibernateException("Exception while invoking " + "lookup method '" + valueOfMethod.getName() + "' of " + "enumeration class '" + enumClass + "'", e);
		}

	}

	@Override
	public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
		try {
			if(value == null) {
				st.setNull(index, type.sqlType());
			} else {
				Object identifier = identifierMethod.invoke(value, new Object[0]);
				type.set(st, identifier, index);
			}
		} catch(Exception e) {
			throw new HibernateException("Exception while invoking " + "identifier method '" + identifierMethod.getName() + "' of " + "enumeration class '" + enumClass + "'", e);
		}
	}

	/**
	 * @param target unused
	 * @param owner unused
	 */
	@Override
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return original;
	}

	@Override
	public Class<?> returnedClass() {
		return enumClass;
	}

	@Override
	public int[] sqlTypes() {
		return sqlTypes;
	}

	@Override
	public void setParameterValues(Properties parameters) {
		String enumClassName = parameters.getProperty("enumClass");
		try {
			enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
		} catch(ClassNotFoundException cnfe) {
			throw new HibernateException("Enum class not found", cnfe);
		}

		String identifierMethodName = parameters.getProperty(ID_METHOD, DEFAULT_SETTER_METHOD);
		try {
			identifierMethod = enumClass.getMethod(identifierMethodName, new Class[0]);
			identifierType = identifierMethod.getReturnType();
		} catch(Exception e) {
			throw new HibernateException("Failed to obtain identifier method", e);
		}

		type = (NullableType) TypeFactory.basic(identifierType.getName());
		if(type == null)
			throw new HibernateException("Unsupported identifier type " + identifierType.getName());
		sqlTypes = new int[] { type.sqlType() };

		String lookupMethodName = parameters.getProperty(VALUE_OF_METHOD, DEFAULT_GETTER_METHOD);

		try {
			valueOfMethod = enumClass.getMethod(lookupMethodName, new Class[] { identifierType });
		} catch(Exception e) {
			throw new HibernateException("Failed to obtain lookup method", e);
		}

	}

}
